package com.hqd.ch03.v48.boot.context.config;

import com.hqd.ch03.utils.ObjectUtils;
import com.hqd.ch03.v48.boot.context.properties.bind.Binder;
import com.hqd.ch03.v48.boot.env.PropertySourceLoader;
import com.hqd.ch03.v48.core.Ordered;
import com.hqd.ch03.v48.io.Resource;
import com.hqd.ch03.v48.io.ResourceLoader;
import com.hqd.ch03.v48.io.support.ClassPathResource;
import com.hqd.ch03.v48.io.support.SpringFactoriesLoader;
import org.apache.commons.lang3.StringUtils;

import java.io.File;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * 符合resource:协议的资源定位器
 */
public class StandardConfigDataLocationResolver
        implements ConfigDataLocationResolver<StandardConfigDataResource>, Ordered {

    /**
     * 配置属性名
     */
    static final String CONFIG_NAME_PROPERTY = "spring.config.name";
    /**
     * 资源协议
     */
    private static final String PREFIX = "resource:";
    /**
     * 默认文件名
     */
    private static final String[] DEFAULT_CONFIG_NAMES = {"application"};
    /**
     * 协议正则
     */
    private static final Pattern URL_PREFIX = Pattern.compile("^([a-zA-Z][a-zA-Z0-9*]*?:)(.*$)");
    /**
     * []正则
     */
    private static final Pattern EXTENSION_HINT_PATTERN = Pattern.compile("^(.*)\\[(\\.\\w+)\\](?!\\[)$");

    private static final String NO_PROFILE = null;
    /**
     * 配置文件加载器
     */
    private final List<PropertySourceLoader> propertySourceLoaders;

    /**
     * 配置文件名
     */
    private final String[] configNames;

    private final LocationResourceLoader resourceLoader;

    public StandardConfigDataLocationResolver(Binder binder, ResourceLoader resourceLoader) {
        this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
                getClass().getClassLoader());
        this.configNames = getConfigNames(binder);
        this.resourceLoader = new LocationResourceLoader(resourceLoader);
    }

    private String[] getConfigNames(Binder binder) {
        /**
         * 获取指定的配置文件名
         */
        String[] configNames = binder.bind(CONFIG_NAME_PROPERTY, String[].class).orElse(DEFAULT_CONFIG_NAMES);
        for (String configName : configNames) {
            validateConfigName(configName);
        }
        return configNames;
    }

    private void validateConfigName(String name) {
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }

    @Override
    public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDataLocation location) {
        return true;
    }

    @Override
    public List<StandardConfigDataResource> resolve(ConfigDataLocationResolverContext context,
                                                    ConfigDataLocation location) throws ConfigDataNotFoundException {
        return resolve(getReferences(context, location.split()));
    }

    private Set<StandardConfigDataReference> getReferences(ConfigDataLocationResolverContext context,
                                                           ConfigDataLocation[] configDataLocations) {
        Set<StandardConfigDataReference> references = new LinkedHashSet<>();
        for (ConfigDataLocation configDataLocation : configDataLocations) {
            references.addAll(getReferences(context, configDataLocation));
        }
        return references;
    }

    private Set<StandardConfigDataReference> getReferences(ConfigDataLocationResolverContext context,
                                                           ConfigDataLocation configDataLocation) {
        /**
         * 获取路径
         */
        String resourceLocation = getResourceLocation(context, configDataLocation);
        try {
            /**
             *是文件夹
             */
            if (isDirectory(resourceLocation)) {
                /**
                 * 获取文件夹下所有资源
                 */
                return getReferencesForDirectory(configDataLocation, resourceLocation, NO_PROFILE);
            }
            /**
             * 获取配置文件
             */
            return getReferencesForFile(configDataLocation, resourceLocation, NO_PROFILE);
        } catch (RuntimeException ex) {
            throw new IllegalStateException("Unable to load config data from '" + configDataLocation + "'", ex);
        }
    }

    @Override
    public List<StandardConfigDataResource> resolveProfileSpecific(ConfigDataLocationResolverContext context,
                                                                   ConfigDataLocation location, Profiles profiles) {
        return resolve(getProfileSpecificReferences(context, location.split(), profiles));
    }

    private Set<StandardConfigDataReference> getProfileSpecificReferences(ConfigDataLocationResolverContext context,
                                                                          ConfigDataLocation[] configDataLocations, Profiles profiles) {
        Set<StandardConfigDataReference> references = new LinkedHashSet<>();
        for (String profile : profiles) {
            for (ConfigDataLocation configDataLocation : configDataLocations) {
                String resourceLocation = getResourceLocation(context, configDataLocation);
                references.addAll(getReferences(configDataLocation, resourceLocation, profile));
            }
        }
        return references;
    }

    private String getResourceLocation(ConfigDataLocationResolverContext context,
                                       ConfigDataLocation configDataLocation) {
        /**
         * 获取资源名
         */
        String resourceLocation = configDataLocation.getNonPrefixedValue(PREFIX);
        /**
         * 是个路径
         */
        boolean isAbsolute = resourceLocation.startsWith("/") || URL_PREFIX.matcher(resourceLocation).matches();
        if (isAbsolute) {
            return resourceLocation;
        }
        /**
         * 从父容器中查找
         */
        ConfigDataResource parent = context.getParent();
        if (parent instanceof StandardConfigDataResource) {
            String parentResourceLocation = ((StandardConfigDataResource) parent).getReference().getResourceLocation();
            String parentDirectory = parentResourceLocation.substring(0, parentResourceLocation.lastIndexOf("/") + 1);
            return parentDirectory + resourceLocation;
        }
        return resourceLocation;
    }

    private Set<StandardConfigDataReference> getReferences(ConfigDataLocation configDataLocation,
                                                           String resourceLocation, String profile) {
        if (isDirectory(resourceLocation)) {
            return getReferencesForDirectory(configDataLocation, resourceLocation, profile);
        }
        return getReferencesForFile(configDataLocation, resourceLocation, profile);
    }

    private Set<StandardConfigDataReference> getReferencesForDirectory(ConfigDataLocation configDataLocation,
                                                                       String directory, String profile) {
        Set<StandardConfigDataReference> references = new LinkedHashSet<>();
        /**
         * 获取所有匹配的文件指定名
         */
        for (String name : this.configNames) {
            Deque<StandardConfigDataReference> referencesForName = getReferencesForConfigName(name, configDataLocation,
                    directory, profile);
            references.addAll(referencesForName);
        }
        return references;
    }

    private Deque<StandardConfigDataReference> getReferencesForConfigName(String name,
                                                                          ConfigDataLocation configDataLocation, String directory, String profile) {
        Deque<StandardConfigDataReference> references = new ArrayDeque<>();
        for (PropertySourceLoader propertySourceLoader : this.propertySourceLoaders) {
            for (String extension : propertySourceLoader.getFileExtensions()) {
                StandardConfigDataReference reference = new StandardConfigDataReference(configDataLocation, directory,
                        directory + name, profile, extension, propertySourceLoader);
                if (!references.contains(reference)) {
                    references.addFirst(reference);
                }
            }
        }
        return references;
    }

    private Set<StandardConfigDataReference> getReferencesForFile(ConfigDataLocation configDataLocation, String file,
                                                                  String profile) {
        Matcher extensionHintMatcher = EXTENSION_HINT_PATTERN.matcher(file);
        boolean extensionHintLocation = extensionHintMatcher.matches();
        /**
         * 是否存在[],存在则去除
         */
        if (extensionHintLocation) {
            file = extensionHintMatcher.group(1) + extensionHintMatcher.group(2);
        }
        for (PropertySourceLoader propertySourceLoader : this.propertySourceLoaders) {
            /**
             * 获取拓展名
             */
            String extension = getLoadableFileExtension(propertySourceLoader, file);
            if (extension != null) {
                /**
                 * 获取全路径，不包含文件格式
                 */
                String root = file.substring(0, file.length() - extension.length() - 1);
                StandardConfigDataReference reference = new StandardConfigDataReference(configDataLocation, null, root,
                        profile, (!extensionHintLocation) ? extension : null, propertySourceLoader);
                return Collections.singleton(reference);
            }
        }
        throw new IllegalStateException("File extension is not known to any PropertySourceLoader. "
                + "If the location is meant to reference a directory, it must end in '/' or File.separator");
    }

    private String getLoadableFileExtension(PropertySourceLoader loader, String file) {
        for (String fileExtension : loader.getFileExtensions()) {
            if (StringUtils.endsWithIgnoreCase(file, fileExtension)) {
                return fileExtension;
            }
        }
        return null;
    }

    /**
     * 以'/'或者文件分隔符结尾，则返回true
     * 这里感觉有点简单粗暴了
     *
     * @param resourceLocation
     * @return
     */
    private boolean isDirectory(String resourceLocation) {
        return resourceLocation.endsWith("/") || resourceLocation.endsWith(File.separator);
    }

    private List<StandardConfigDataResource> resolve(Set<StandardConfigDataReference> references) {
        List<StandardConfigDataResource> resolved = new ArrayList<>();
        for (StandardConfigDataReference reference : references) {
            resolved.addAll(resolve(reference));
        }
        if (resolved.isEmpty()) {
            resolved.addAll(resolveEmptyDirectories(references));
        }
        return resolved;
    }

    private Collection<StandardConfigDataResource> resolveEmptyDirectories(
            Set<StandardConfigDataReference> references) {
        Set<StandardConfigDataResource> empty = new LinkedHashSet<>();
        for (StandardConfigDataReference reference : references) {
            if (reference.getDirectory() != null) {
                empty.addAll(resolveEmptyDirectories(reference));
            }
        }
        return empty;
    }

    private Set<StandardConfigDataResource> resolveEmptyDirectories(StandardConfigDataReference reference) {
        if (!this.resourceLoader.isPattern(reference.getResourceLocation())) {
            return resolveNonPatternEmptyDirectories(reference);
        }
        return resolvePatternEmptyDirectories(reference);
    }

    private Set<StandardConfigDataResource> resolveNonPatternEmptyDirectories(StandardConfigDataReference reference) {
        Resource resource = this.resourceLoader.getResource(reference.getDirectory());
        return (resource instanceof ClassPathResource || !resource.exists()) ? Collections.emptySet()
                : Collections.singleton(new StandardConfigDataResource(reference, resource, true));
    }

    private Set<StandardConfigDataResource> resolvePatternEmptyDirectories(StandardConfigDataReference reference) {
        Resource[] subdirectories = this.resourceLoader.getResources(reference.getDirectory(), LocationResourceLoader.ResourceType.DIRECTORY);
        ConfigDataLocation location = reference.getConfigDataLocation();
        if (!location.isOptional() && ObjectUtils.isEmpty(subdirectories)) {
            String message = String.format("Config data location '%s' contains no subdirectories", location);
            throw new ConfigDataLocationNotFoundException(location, message, null);
        }
        return Arrays.stream(subdirectories).filter(Resource::exists)
                .map((resource) -> new StandardConfigDataResource(reference, resource, true))
                .collect(Collectors.toCollection(LinkedHashSet::new));
    }

    private List<StandardConfigDataResource> resolve(StandardConfigDataReference reference) {
        if (!this.resourceLoader.isPattern(reference.getResourceLocation())) {
            return resolveNonPattern(reference);
        }
        return resolvePattern(reference);
    }

    private List<StandardConfigDataResource> resolveNonPattern(StandardConfigDataReference reference) {
        Resource resource = this.resourceLoader.getResource(reference.getResourceLocation());
        if (!resource.exists() && reference.isSkippable()) {
            logSkippingResource(reference);
            return Collections.emptyList();
        }
        return Collections.singletonList(createConfigResourceLocation(reference, resource));
    }

    private List<StandardConfigDataResource> resolvePattern(StandardConfigDataReference reference) {
        List<StandardConfigDataResource> resolved = new ArrayList<>();
        for (Resource resource : this.resourceLoader.getResources(reference.getResourceLocation(), LocationResourceLoader.ResourceType.FILE)) {
            if (!resource.exists() && reference.isSkippable()) {
                logSkippingResource(reference);
            } else {
                resolved.add(createConfigResourceLocation(reference, resource));
            }
        }
        return resolved;
    }

    private void logSkippingResource(StandardConfigDataReference reference) {
    }

    private StandardConfigDataResource createConfigResourceLocation(StandardConfigDataReference reference,
                                                                    Resource resource) {
        return new StandardConfigDataResource(reference, resource);
    }

}
